"""
nose plugin for easy testing of django projects and apps. Sets up a test
database (or schema) and installs apps from test settings file before tests
are run, and tears the test database (or schema) down after all tests are run.
"""
__author = 'Jason Pellerin'
__version__ = '0.1'

import atexit
import logging
import os
import re

from nose.plugins import Plugin

log = logging.getLogger('nose.plugins.nosedjango')

class NoseDjango(Plugin):
    """
    Enable to set up django test environment before running all tests, and
    tear it down after all tests. If the django database engine in use is not
    sqlite3, one or both of --django-test-db or django-test-schema must be
    specified.

    Note that your django project must be on PYTHONPATH for the settings file
    to be loaded. The plugin will help out by placing the nose working dir
    into sys.path if it isn't already there, unless the -P
    (--no-path-adjustment) argument is set.
    """
    name = 'django'
    # FIXME make this a list
    ignoreDjangoFiles = re.compile(r'^(manage\.py|.*settings\.py)$')
    env = None
    old_db = None
    test_db = None
    test_schema = None
    created_db = None
    created_schema = None
    
    def add_options(self, parser, env=os.environ):
        # keep copy so we can make our setting in the env we were passed
        self.env = env
        Plugin.add_options(self, parser, env)
        parser.add_option('--django-settings', action='store',
                          dest='django_settings', default=None,
                          help="Set module as the DJANGO_SETTINGS_MODULE"
                          " environment variable.")
        parser.add_option('--django-test-db', action='store',
                          dest='django_test_db',
                          default=env.get('NOSE_DJANGO_TEST_DB'),
                          help='Create this database for use in tests and '
                          'drop it after tests have run. May be combined '
                          'with --django-test-schema option for databases '
                          'that support schemas. This option is not used for '
                          'sqlite3 connections. PLEASE NOTE that if the test '
                          'database already exists, the test run will be '
                          'immediately cancelled. '
                          '[NOSE_DJANGO_TEST_DB]')
        parser.add_option('--django-test-schema', action='store',
                          dest='django_test_schema',
                          default=env.get('NOSE_DJANGO_TEST_SCHEMA'),
                          help='Create this schema for use in tests and '
                          'drop it after tests have run. May be combined '
                          'with --django-test-db; otherwise, test schema '
                          'will be created in the database named by '
                          'DATABASE_NAME in the active django settings. '
                          'This option is not used for sqlite3 connections. '
                          'PLEASE NOTE that if the specified schema already '
                          'exists in the target database, the test run will '
                          'be immediately cancelled. '
                          '[NOSE_DJANGO_TEST_SCHEMA]')
        
    def configure(self, options, conf):
        Plugin.configure(self, options, conf)
        if self.enabled:
            # insert my ignores into conf.ignoreFiles, to force main
            # system to ignore django scripts and settings files, no
            # matter what other plugins say
            conf.ignoreFiles.append(self.ignoreDjangoFiles)
        if options.django_settings and self.env is not None:
            self.env['DJANGO_SETTINGS_MODULE'] = options.django_settings
        if options.django_test_db:
            self.test_db = options.django_test_db
        if options.django_test_schema:
            self.test_schema = options.django_test_schema

    def begin(self):
        """Create the test database and schema, if needed, and switch the
        connection over to that database. Then call install() to install
        all apps listed in the loaded settings module.
        """
        # Add the working directory (and any package parents) to sys.path
        # before trying to import django modules; otherwise, they won't be
        # able to find project.settings if the working dir is project/ or
        # project/..
        if self.conf.addPaths:
            from nose.importer import add_path
            map(add_path, self.conf.where)
        
        from django.conf import settings
        from django.db import connection        
        from django.core import management
        from django.db.models.loading import get_apps
        self.old_db = settings.DATABASE_NAME

        # make sure we can clean up after ourselves
        atexit.register(self.finalize)
        
        if settings.DATABASE_ENGINE == 'sqlite3':
            # always use a :memory: database for sqlite
            self.test_db = ':memory:'
        else:
            if not self.test_db and not self.test_schema:
                raise Exception("When using a database engine other than "
                                "sqlite3, one or both of --django-test-db "
                                "and --django-test-schema must be specified")
            if self.test_db:
                # create test db for others, if requested
                cursor = connection.cursor()
                connection.connection.autocommit(1)
                cursor.execute("CREATE DATABASE %s" % self.test_db)
                log.info("Created test database %s", self.test_db)
                self.created_db = self.test_db
        # connect to the test db, if desired
        if self.test_db:
            connection.close()
            settings.DATABASE_NAME = self.test_db
        # pg only: create test schema, set as default in search path
        # FIXME: throw error now if not pg compat?
        if self.test_schema:
            cursor = connection.cursor()
            connection.connection.autocommit(1)
            cursor.execute("CREATE SCHEMA %s" % self.test_schema)
            cursor.execute("SET search_path TO %s,pg_catalog" %
                           self.test_schema)
            log.info("Created test schema %s", self.test_schema)
            self.created_schema = self.test_schema
        # sync up the db
        for app in get_apps():
            log.info("Installing django app %s", app)
            management.install(app)
            
    def finalize(self, result=None):
        """Clean up any created database and schema.
        """
        from django.conf import settings
        from django.db import connection
        import time
        
        if self.created_schema:
            cursor = connection.cursor()
            connection.connection.autocommit(1)
            cursor.execute("DROP SCHEMA %s CASCADE" % self.created_schema)
            log.info("Dropped schema %s", self.created_schema)
            self.created_schema = None # in case run again

        if self.created_db:
            connection.close()
            time.sleep(1) # avoid "being access by other users" error
            settings.DATABASE_NAME = self.old_db
            cursor = connection.cursor()
            connection.connection.autocommit(1)
            cursor.execute("DROP DATABASE %s" % self.created_db)
            log.info("Dropped database %s", self.created_db)
            self.created_db = None # in case run again
